CLUSTERING
from pathlib import Path
# définir le répertoire du projet contenant le dossier data/ et notebooks/
HOME = Path.cwd().parent
print(f"Home directory: {HOME}")
# définir le répertoire des données
DATA = Path(HOME, "data")
print(f"Data directory: {DATA}")
Home directory: C:\Users\DELL\Desktop\Projet ML2 Data directory: C:\Users\DELL\Desktop\Projet ML2\data
! pip install pywaffle
Collecting pywaffle
Downloading pywaffle-1.1.0-py2.py3-none-any.whl (30 kB)
Collecting fontawesomefree (from pywaffle)
Downloading fontawesomefree-6.5.1-py3-none-any.whl (25.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 25.6/25.6 MB 47.7 MB/s eta 0:00:00
Requirement already satisfied: matplotlib in /usr/local/lib/python3.10/dist-packages (from pywaffle) (3.7.1)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (1.2.1)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (4.53.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (1.4.5)
Requirement already satisfied: numpy>=1.20 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (1.25.2)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (24.0)
Requirement already satisfied: pillow>=6.2.0 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (9.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (3.1.2)
Requirement already satisfied: python-dateutil>=2.7 in /usr/local/lib/python3.10/dist-packages (from matplotlib->pywaffle) (2.8.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-dateutil>=2.7->matplotlib->pywaffle) (1.16.0)
Installing collected packages: fontawesomefree, pywaffle
Successfully installed fontawesomefree-6.5.1 pywaffle-1.1.0
! pip install category_encoders
Collecting category_encoders
Downloading category_encoders-2.6.3-py2.py3-none-any.whl (81 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 81.9/81.9 kB 952.6 kB/s eta 0:00:00
Requirement already satisfied: numpy>=1.14.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (1.25.2)
Requirement already satisfied: scikit-learn>=0.20.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (1.2.2)
Requirement already satisfied: scipy>=1.0.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (1.11.4)
Requirement already satisfied: statsmodels>=0.9.0 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (0.14.2)
Requirement already satisfied: pandas>=1.0.5 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (2.0.3)
Requirement already satisfied: patsy>=0.5.1 in /usr/local/lib/python3.10/dist-packages (from category_encoders) (0.5.6)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.10/dist-packages (from pandas>=1.0.5->category_encoders) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.10/dist-packages (from pandas>=1.0.5->category_encoders) (2023.4)
Requirement already satisfied: tzdata>=2022.1 in /usr/local/lib/python3.10/dist-packages (from pandas>=1.0.5->category_encoders) (2024.1)
Requirement already satisfied: six in /usr/local/lib/python3.10/dist-packages (from patsy>=0.5.1->category_encoders) (1.16.0)
Requirement already satisfied: joblib>=1.1.1 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=0.20.0->category_encoders) (1.4.2)
Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.10/dist-packages (from scikit-learn>=0.20.0->category_encoders) (3.5.0)
Requirement already satisfied: packaging>=21.3 in /usr/local/lib/python3.10/dist-packages (from statsmodels>=0.9.0->category_encoders) (24.0)
Installing collected packages: category_encoders
Successfully installed category_encoders-2.6.3
%pylab inline
import pandas as pd
import seaborn as sns
import plotly.graph_objs as go
from pywaffle import Waffle
from sklearn.base import clone
from sklearn.pipeline import Pipeline
from sklearn.cluster import KMeans
from sklearn.preprocessing import MinMaxScaler, StandardScaler
from sklearn.metrics import silhouette_score, adjusted_rand_score
import category_encoders as ce
from sklearn.decomposition import PCA
from time import time
from yellowbrick.cluster import KElbowVisualizer
from yellowbrick.cluster import silhouette_visualizer
import operator
from sklearn import manifold
import plotly.express as px
from sklearn.mixture import GaussianMixture
import datetime as dt
from dateutil.relativedelta import relativedelta
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_rows', 100)
pd.set_option('display.max_columns', 200)
plt.style.use('fivethirtyeight')
%pylab is deprecated, use %matplotlib inline and import the required libraries. Populating the interactive namespace from numpy and matplotlib
db_df = pd.read_csv(Path(DATA, "data.csv"), sep=",")
db_df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 119143 entries, 0 to 119142 Data columns (total 43 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 customer_id 119143 non-null object 1 customer_unique_id 119143 non-null object 2 customer_zip_code_prefix 119143 non-null int64 3 customer_city 119143 non-null object 4 customer_state 119143 non-null object 5 order_id 119143 non-null object 6 order_status 119143 non-null object 7 order_purchase_timestamp 119143 non-null object 8 order_approved_at 118966 non-null object 9 order_delivered_carrier_date 117057 non-null object 10 order_delivered_customer_date 115722 non-null object 11 order_estimated_delivery_date 119143 non-null object 12 delivery_time 115722 non-null float64 13 delivery_delay 115722 non-null float64 14 order_item_id 118310 non-null float64 15 product_id 118310 non-null object 16 seller_id 118310 non-null object 17 shipping_limit_date 118310 non-null object 18 price 118310 non-null float64 19 freight_value 118310 non-null float64 20 product_category_name 118310 non-null object 21 product_name_lenght 118310 non-null float64 22 product_description_lenght 118310 non-null float64 23 product_photos_qty 118310 non-null float64 24 product_weight_g 118290 non-null float64 25 product_length_cm 118290 non-null float64 26 product_height_cm 118290 non-null float64 27 product_width_cm 118290 non-null float64 28 seller_zip_code_prefix 118310 non-null float64 29 seller_city 118310 non-null object 30 seller_state 118310 non-null object 31 review_id 118146 non-null object 32 review_score 118146 non-null float64 33 review_comment_title 13989 non-null object 34 review_comment_message 50244 non-null object 35 review_creation_date 118146 non-null object 36 review_answer_timestamp 118146 non-null object 37 payment_sequential 119140 non-null float64 38 payment_type 119140 non-null object 39 payment_installments 119140 non-null float64 40 payment_value 119140 non-null float64 41 product_category_name_english 118310 non-null object 42 product_category_agg 116576 non-null object dtypes: float64(17), int64(1), object(25) memory usage: 39.1+ MB
Computing final features¶
def mode_w_nan(series):
if all(([x is np.nan for x in series])):
return 'unknown'
return pd.Series.mode(series)[0] # if multiple items are returned, take the first
def compute_features(db_df):
'''Compute the features of the final dataset, same way as the previous notebook (without correlated features and categorical features with too many unique values)
db_df: DataFrame - Grouped tables
output: Dataframe - Final dataset
'''
for col in db_df.columns:
# dates as DateTime
if any([sub in col for sub in ['date', '_at', 'timestamp']]):
# Try to infer the datetime format automatically
db_df[col] = pd.to_datetime(db_df[col], errors='coerce')
# number of orders per customer
df = pd.DataFrame(data={'nb_orders': db_df.groupby('customer_unique_id')['customer_id'].nunique(dropna=True)})
# total number of products ordered per customer
df['total_products_ordered'] = db_df.groupby(['customer_unique_id', 'order_id'])['order_item_id'].max().groupby('customer_unique_id').sum()
# total paid
df['total_paid'] = db_df.groupby('customer_unique_id')['price'].sum()
# mean description length for ordered products
# df['mean_prod_descr_length'] = db_df.groupby('customer_unique_id')['product_description_lenght'].mean()
# mean photos qty for ordered products
df['mean_photo_qty_for_ordered_prods'] = db_df.groupby('customer_unique_id')['product_photos_qty'].mean()
# mean review score
df['mean_reviews_score'] = db_df.groupby('customer_unique_id')['review_score'].mean()
# first order date
first_order_date = db_df.groupby('customer_unique_id')['order_purchase_timestamp'].min()
# last order date
last_order_date = db_df.groupby('customer_unique_id')['order_purchase_timestamp'].max()
# days since the last order
df['recency'] = pd.to_timedelta(db_df['order_purchase_timestamp'].max() - last_order_date).dt.days
# mean delivery time
df['mean_delivery_time'] = db_df.groupby('customer_unique_id')['delivery_time'].mean(numeric_only=False)
# mean delivery delay
# df['mean_delivery_delay'] = db_df.groupby('customer_unique_id')['delivery_delay'].mean(numeric_only=False)
# favorite product category
df['favorite_category'] = db_df.groupby('customer_unique_id')['product_category_agg'].agg(mode_w_nan)
# favorite payment type
df['favorite_payment_type'] = db_df.groupby('customer_unique_id')['payment_type'].agg(mode_w_nan)
# customer state
df = df.join(db_df[['customer_unique_id', 'customer_state']].drop_duplicates(subset=['customer_unique_id']).set_index('customer_unique_id'))
# fill null values caused when trying to divide by 0
for col in df.columns:
if 'mean' in col:
df[col].fillna(0, inplace=True)
return df
df = compute_features(db_df)
df.info()
<class 'pandas.core.frame.DataFrame'> Index: 96096 entries, 0000366f3b9a7992bf8c76cfdf3221e2 to ffffd2657e2aad2907e67c3e9daecbeb Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 nb_orders 96096 non-null int64 1 total_products_ordered 96096 non-null float64 2 total_paid 96096 non-null float64 3 mean_photo_qty_for_ordered_prods 96096 non-null float64 4 mean_reviews_score 96096 non-null float64 5 recency 96096 non-null int64 6 mean_delivery_time 96096 non-null float64 7 favorite_category 96096 non-null object 8 favorite_payment_type 96096 non-null object 9 customer_state 96096 non-null object dtypes: float64(5), int64(2), object(3) memory usage: 8.1+ MB
RFM¶
def rfm_iso_scatter(data, x, y, z, color='nb_orders', reduce=True):
'''generate a 3D scatter plot
Input:
- dat : dataframe
- x, y, z : str - 3 features name for the three coordinates
'''
if reduce:
x = data[x]
y = data[data[y] < 10][y]
z = data[data[z] < 4000][z]
else:
x = data[x]
y = data[y]
z = data[z]
fig = go.Figure(data=[go.Scatter3d(x=x, y=y, z=z, mode='markers', marker=dict(size=1, color=data[color], colorscale='thermal', opacity=0.8))])
fig.update_layout(scene=dict(
xaxis_title='Recency',
yaxis_title='Frequency',
zaxis_title='Monetary'),
width=800,
margin=dict(r=20, b=10, l=10, t=10))
fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))
fig.show()
rfm_iso_scatter(df, 'recency', 'nb_orders', 'total_paid')
I. K-MEANS
A. Prepocessing
1. Encoding & Scaling
categorical_cols = df.columns[df.dtypes == object].tolist()
preprocessor = Pipeline([
('encoding', ce.OneHotEncoder(cols=categorical_cols)),
('features_scaling', MinMaxScaler()) # MinMaxScaler because the shape of all features do not follows a normal distribution
])
data_scaled = preprocessor['encoding'].fit_transform(df)
2. PCA
n_components = min(data_scaled.shape[0], data_scaled.shape[1])
# addind a PCA step in the preprocessor pipeline
preprocessor.steps.append(['pca', PCA(n_components=n_components, random_state=0)])
preprocessor.fit(df)
Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca', PCA(n_components=52, random_state=0)]])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca', PCA(n_components=52, random_state=0)]])OneHotEncoder(cols=['favorite_category', 'favorite_payment_type',
'customer_state'])MinMaxScaler()
PCA(n_components=52, random_state=0)
fig, ax = plt.subplots(figsize=(12, 7))
plt.plot(np.arange(0, n_components), preprocessor['pca'].explained_variance_ratio_.cumsum())
plt.title("Explained variance vs. number of components", fontsize=16)
plt.ylabel("Cumsum explained variance ratio")
plt.xlabel("Factor number")
plt.xticks(range(0, n_components+1, 5))
plt.show()
Plus de 90% de la variance a été expliquée par 26 composants
# Assuming 'df' has 26 or fewer features, set n_components to a value less than or equal to 26
preprocessor['pca'].n_components = 26
data_pca = preprocessor.fit_transform(df)
data_pca.shape
(96096, 26)
data_pca
array([[-0.34100803, 0.71395258, 0.71899138, ..., 0.00116628,
-0.01011132, 0.00599096],
[-0.31724919, 0.60822985, -0.30847366, ..., -0.01193224,
-0.01172388, -0.00349919],
[-0.28909382, -0.38698585, -0.05458112, ..., -0.05797138,
0.0198105 , -0.04046975],
...,
[-0.29688053, -0.33439444, -0.07436493, ..., 0.06207249,
0.02535949, 0.12949427],
[-0.31456381, -0.34651048, -0.0973208 , ..., -0.22907343,
-0.01619359, -0.11797202],
[-0.30970875, -0.38453026, -0.2217024 , ..., -0.03247049,
0.01727301, -0.02586894]])
# Using 2 first components of the PCA
plt.figure(figsize=(10, 7))
plt.scatter(data_pca[:, 0], data_pca[:, 1], s=30)
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.show()
3. Nombre optimal de Cluster
kmeans_pipe = Pipeline([
('preprocessor', preprocessor),
('clusterer', KMeans())
])
kmeans_pipe
Pipeline(steps=[('preprocessor',
Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca',
PCA(n_components=26, random_state=0)]])),
('clusterer', KMeans())])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocessor',
Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca',
PCA(n_components=26, random_state=0)]])),
('clusterer', KMeans())])Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca', PCA(n_components=26, random_state=0)]])OneHotEncoder(cols=['favorite_category', 'favorite_payment_type',
'customer_state'])MinMaxScaler()
PCA(n_components=26, random_state=0)
KMeans()
def display_kmeans_results(pipeline, data):
t0 = time()
pipeline.fit(data)
fit_time = time() - t0
results = [kmeans_pipe['clusterer'].init, fit_time, kmeans_pipe['clusterer'].n_iter_, kmeans_pipe['clusterer'].inertia_]
kmeans_data_pca = kmeans_pipe['preprocessor'].transform(df)
kmeans_pred_labels = kmeans_pipe['clusterer'].labels_
results += [silhouette_score(kmeans_data_pca, kmeans_pred_labels, metric='euclidean')]
print('init\t\ttime\tnb_iter\tinertia\tsilhouette')
print("{:9s}\t{:.3f}s\t{}\t{:.0f}\t{:.3f}".format(*results))
La fonction display_kmeans_results entraîne un pipeline KMeans sur un jeu de données, mesure le temps d'entraînement, et collecte des métriques de performance clés telles que la méthode d'initialisation, le nombre d'itérations jusqu'à la convergence, l'inertie et le score silhouette. Elle transforme les données, prédit les labels des clusters, et affiche ces résultats de manière formatée pour évaluer la qualité et l'efficacité du clustering réalisé.
display_kmeans_results(kmeans_pipe, df)
init time nb_iter inertia silhouette k-means++ 7.121s 8 115375 0.200
Elbow method
def display_elbow_visu(pipeline, data, k_range, metric, elbow=True):
pipeline.fit(data)
(fig, ax) = plt.subplots(figsize=(10, 7))
visualizer_WCSS = KElbowVisualizer(clone(pipeline['clusterer']), k=k_range, metric=metric, locate_elbow=elbow, ax=ax)
visualizer_WCSS.fit(pipeline['preprocessor'].fit_transform(data))
visualizer_WCSS.show()
k_range = (2, 11)
display_elbow_visu(kmeans_pipe, df, k_range, 'distortion')
Le score de distorsion $ W(C) $ pour un ensemble de clusters $ C = \{C_1, C_2, \ldots, C_k\} $ avec leurs centroides respectifs $ \mu_1, \mu_2, \ldots, \mu_k $ est défini comme :
$ W(C) = \sum_{i=1}^k \sum_{x \in C_i} \| x - \mu_i \|^2 $
$ C_i $ représente le i-ème cluster.
$ x $ est un point de données dans le cluster $ C_i $.
$ \mu_i $ est le centroïde du cluster $ C_i $.
$ \| x - \mu_i \|^2 $ est la distance euclidienne au carré entre le point $ x $ et le centroïde $ \mu_i $.
Cette formule quantifie la somme des distances au carré de tous les points de données à leurs centroides respectifs dans leurs clusters assignés.
%%time
display_elbow_visu(kmeans_pipe, df, k_range, 'silhouette', elbow=False)
CPU times: total: 31min 16s Wall time: 21min 13s
Analyse de la silhouette
def silouhette_analysis(pipeline, data, k_range):
scores_dict = {}
for i in range(k_range[0], k_range[1]):
pipeline['clusterer'].n_clusters = i
(fig, ax) = plt.subplots()
visu = silhouette_visualizer(clone(pipeline['clusterer']), pipeline['preprocessor'].fit_transform(data), colors='yellowbrick', ax=ax)
scores_dict[i] = visu.silhouette_score_ # index = number of clusters / value = silouette score
for item in scores_dict:
print('{} clusters - Average silhouette score : {}'.format(item, scores_dict[item]))
return scores_dict
%%time
scores = silouhette_analysis(kmeans_pipe, df, (4, 11))
4 clusters - Average silhouette score : 0.1584443451685507 5 clusters - Average silhouette score : 0.1840502897531775 6 clusters - Average silhouette score : 0.1761567551139147 7 clusters - Average silhouette score : 0.21194686181308134 8 clusters - Average silhouette score : 0.19908050027228946 9 clusters - Average silhouette score : 0.2025438876158195 10 clusters - Average silhouette score : 0.21510648809841731 CPU times: total: 48min 42s Wall time: 39min 17s
La méthode Elbow nous permet de choisir k = 5
best_sil_score = max(scores.items(), key=operator.itemgetter(1))[0]
print('Best average silhouette score : {} clusters ({:.4})'.format(best_sil_score, scores[best_sil_score]))
Best average silhouette score : 10 clusters (0.2151)
# set the number of clusters which produce the best average silouhette score
kmeans_pipe['clusterer'].n_clusters = 5
4. Visualisation
PCA reduced data
def visualize_kmeans_clustering(pipeline, data, pca=False):
reduced_data = pipeline['preprocessor'].fit_transform(data)
if pca:
reduced_data = PCA(n_components=2).fit_transform(reduced_data)
pipeline['clusterer'].fit(reduced_data[:, :2])
# Step size of the mesh. Decrease to increase the quality of the VQ.
h = .02 # point in the mesh [x_min, x_max]x[y_min, y_max].
# Plot the decision boundary. For that, we will assign a color to each
x_min, x_max = reduced_data[:, 0].min() - 1, reduced_data[:, 0].max() + 1
y_min, y_max = reduced_data[:, 1].min() - 1, reduced_data[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
# Obtain labels for each point in mesh.
Z = pipeline['clusterer'].predict(np.c_[xx.ravel(), yy.ravel()])
# Put the result into a color plot
Z = Z.reshape(xx.shape)
plt.figure(1, figsize=(15, 12))
plt.clf()
plt.imshow(Z, interpolation="nearest",
extent=(xx.min(), xx.max(), yy.min(), yy.max()),
cmap=plt.cm.Paired, aspect="auto", origin="lower")
plt.plot(reduced_data[:, 0], reduced_data[:, 1], 'k.', markersize=2)
# Plot the centroids as a white X
centroids = pipeline['clusterer'].cluster_centers_
plt.scatter(centroids[:, 0], centroids[:, 1], marker="x", s=50, linewidths=2, color="w", zorder=10)
plt.title("K-means clustering on the OLIST dataset (PCA-Reduced)\nWith {} clusters".format(pipeline['clusterer'].n_clusters))
plt.xlim(x_min, x_max)
plt.ylim(y_min, y_max)
plt.xticks(())
plt.yticks(())
plt.show()
Ce fonction entraîne un pipeline de clustering K-means sur des données réduites via un préprocesseur (et éventuellement via une analyse en composantes principales, PCA), puis visualise les frontières de décision des clusters sur un graphique 2D. Il transforme les données d'entrée en un espace réduit à deux dimensions, ajuste le modèle K-means, et génère une grille de points couvrant cet espace. Il prédit les labels des clusters pour chaque point de la grille et affiche les résultats sous forme de carte de couleur. Les points d'origine des données sont tracés en noir et les centroïdes des clusters sont indiqués par des croix blanches. Le graphique est annoté avec le nombre de clusters utilisés et montre les limites des clusters déterminées par l'algorithme K-means.
visualize_kmeans_clustering(kmeans_pipe, df)
T-SNE
Cette fonction réalise une visualisation des clusters en utilisant la réduction de dimension avec l'algorithme t-SNE. Il transforme les données en deux dimensions pour permettre une visualisation 2D des clusters, en utilisant les paramètres définis dans tsne_params. Les résultats de cette transformation sont stockés dans un DataFrame, où chaque point est coloré selon son étiquette de cluster prédite. Une figure est ensuite générée avec seaborn, montrant un graphique de dispersion des points, ce qui permet de visualiser la répartition et la séparation des clusters dans l'espace réduit.
tsne_params = {
'n_components': 2,
'init': 'pca',
'perplexity': 5,
'n_iter': 1000,
'n_iter_without_progress': 300,
'learning_rate': 200.,
'n_jobs': -1,
'random_state': 0
}
def tsne_visualization(data, predicted_labels, tsne_params):
tsne = manifold.TSNE(**tsne_params)
tsne_result = tsne.fit_transform(data)
df_tsne = pd.DataFrame(tsne_result, columns=['tsne_1', 'tsne_2'])
df_tsne['predicted_labels'] = predicted_labels
plt.figure(figsize=(15, 12))
sns.scatterplot(
x='tsne_1', y='tsne_2',
hue='predicted_labels',
palette=sns.color_palette("hls", df_tsne['predicted_labels'].nunique()),
data=df_tsne,
legend="full",
alpha=0.4)
%%time
kmeans_pipe.fit(df)
tsne_visualization(kmeans_pipe['preprocessor'].transform(df), kmeans_pipe['clusterer'].labels_, tsne_params)
CPU times: total: 30min 2s Wall time: 17min 10s
5. Interprêtation des clusters
# add predicted labels to the inital dataset
kmeans_pipe.fit(df)
df_labels = df.copy()
df_labels['predicted_label'] = kmeans_pipe['clusterer'].labels_
def clustering_interpretations(df, min_max=(0, 100)):
grouped_df = df.select_dtypes(include=['number']).groupby('predicted_label').mean()
scaler = MinMaxScaler(min_max)
scaled_df = pd.DataFrame(scaler.fit_transform(grouped_df), columns=grouped_df.columns)
scores = []
for cluster, row in scaled_df.iterrows():
print(row)
scores.append(row)
fig = px.line_polar(row, r=row.values, theta=row.index, line_close=True, title='Cluster {}'.format(cluster))
fig.update_traces(fill='toself')
fig.show()
return pd.DataFrame(scores)
clusters_descr = clustering_interpretations(df_labels)
nb_orders 67.910687 total_products_ordered 100.000000 total_paid 37.351216 mean_photo_qty_for_ordered_prods 0.000000 mean_reviews_score 56.542820 recency 75.363783 mean_delivery_time 77.366792 Name: 0, dtype: float64
nb_orders 85.760076 total_products_ordered 71.408393 total_paid 27.315241 mean_photo_qty_for_ordered_prods 13.873472 mean_reviews_score 100.000000 recency 0.000000 mean_delivery_time 0.000000 Name: 1, dtype: float64
nb_orders 47.078506 total_products_ordered 45.214066 total_paid 63.718499 mean_photo_qty_for_ordered_prods 28.945336 mean_reviews_score 0.000000 recency 88.902617 mean_delivery_time 100.000000 Name: 2, dtype: float64
nb_orders 100.000000 total_products_ordered 86.890062 total_paid 0.000000 mean_photo_qty_for_ordered_prods 45.024621 mean_reviews_score 70.287270 recency 100.000000 mean_delivery_time 72.511438 Name: 3, dtype: float64
nb_orders 0.000000 total_products_ordered 0.000000 total_paid 100.000000 mean_photo_qty_for_ordered_prods 100.000000 mean_reviews_score 84.839486 recency 85.351412 mean_delivery_time 79.503168 Name: 4, dtype: float64
- Cluster 0 : Clients fidèles à haute commande
- Caractéristiques principales :
- Nombre de commandes : 67.91
- Produits commandés : 100.00
- Total payé : 37.35
- Quantité moyenne de photos pour les produits commandés : 0.00
- Note moyenne des avis : 56.54
- Récence : 75.36
- Temps moyen de livraison : 77.37
- Stratégies :
- Fidélisation : Proposer des récompenses pour les achats fréquents, comme des programmes de points ou des cartes de fidélité.
- Optimisation des livraisons : Améliorer les délais de livraison pour accroître la satisfaction.
- Caractéristiques principales :
- Cluster 1 : Clients très satisfaits et réguliers
- Caractéristiques principales :
- Nombre de commandes : 85.76
- Produits commandés : 71.41
- Total payé : 27.32
- Quantité moyenne de photos pour les produits commandés : 13.87
- Note moyenne des avis : 100.00 (très élevé)
- Récence : 0.00 (ancien)
- Temps moyen de livraison : 0.00
- Stratégies :
- Fidélisation : Proposer des offres exclusives pour les inciter à continuer leurs achats.
- Engagement : Maintenir un contact régulier avec des campagnes de marketing par email.
- Caractéristiques principales :
- Cluster 2 : Clients à forte dépense mais à faible activité récente
- Caractéristiques principales :
- Nombre de commandes : 47.08
- Produits commandés : 45.21
- Total payé : 63.72
- Quantité moyenne de photos pour les produits commandés : 28.95
- Note moyenne des avis : 0.00
- Récence : 88.90
- Temps moyen de livraison : 100.00
- Stratégies :
- Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir, comme des emails ou des notifications push.
- Incitations : Offrir des réductions ou des avantages pour leur premier achat après une période d'inactivité.
- Caractéristiques principales :
- Cluster 3 : Clients réguliers mais à faible dépense récente
- Caractéristiques principales :
- Nombre de commandes : 100.00
- Produits commandés : 86.89
- Total payé : 0.00
- Quantité moyenne de photos pour les produits commandés : 45.02
- Note moyenne des avis : 70.29
- Récence : 100.00
- Temps moyen de livraison : 72.51
- Stratégies :
- Incitations : Offrir des promotions pour augmenter leurs dépenses.
- Recommandations personnalisées : Proposer des produits basés sur leurs historiques d'achats.
- Caractéristiques principales :
- Cluster 4 : Clients à haute dépense mais inactifs
- Caractéristiques principales :
- Nombre de commandes : 0.00
- Produits commandés : 0.00
- Total payé : 100.00 (très élevé)
- Quantité moyenne de photos pour les produits commandés : 100.00
- Note moyenne des avis : 84.84
- Récence : 85.35
- Temps moyen de livraison : 79.50
- Stratégies :
- Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir, comme des emails ou des notifications push.
- Incitations : Offrir des réductions ou des avantages pour leur premier achat après une période d'inactivité.
- Caractéristiques principales :
6. Stabilisation du modèle
def test_init_stability(pipeline, data, nb_tests):
# unfix random_state
if pipeline['clusterer'].random_state:
pipeline['clusterer'].random_state = None
# baseline model
pipeline.fit(data)
ref_labels = pipeline['clusterer'].labels_
tested_models_dict = {}
for i in range(1, nb_tests+1):
current_test = {}
pipeline.fit(data)
# store current test model
current_test['model'] = pipeline['clusterer']
# store ari score
current_test['ari_score'] = adjusted_rand_score(ref_labels, current_test['model'].labels_)
# store current test in the dict to return
tested_models_dict[i] = current_test
print('Test {} - ARI : {}'.format(i, current_test['ari_score']))
return tested_models_dict
tested_models = test_init_stability(kmeans_pipe, df, 20)
Test 1 - ARI : 0.21338664296635068 Test 2 - ARI : 0.25900956616931803 Test 3 - ARI : 0.4087918683487351 Test 4 - ARI : 0.5571098367062934 Test 5 - ARI : 0.35960315775069857 Test 6 - ARI : 0.7918331157641595 Test 7 - ARI : 0.3686104795533704 Test 8 - ARI : 0.4626572775244739 Test 9 - ARI : 0.28186317970078656 Test 10 - ARI : 0.3078532577476168 Test 11 - ARI : 0.22034780038828441 Test 12 - ARI : 0.429158284217817 Test 13 - ARI : 0.22391096152908005 Test 14 - ARI : 0.19667435144652914 Test 15 - ARI : 0.32509458119852064 Test 16 - ARI : 0.5125770872684193 Test 17 - ARI : 0.35003854742854923 Test 18 - ARI : 0.35017904227206315 Test 19 - ARI : 0.33417803497923165 Test 20 - ARI : 0.2989457031188324
Optimisation des Hyperparamètres
Avec ce nombre de clusters, quelle est la combinaision des autres hyperparamètres qui permettent de trouver un bon modèle.
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import silhouette_samples, silhouette_score
kmeans_pipe_2 = Pipeline([
('preprocessor', preprocessor),
('clusterer', KMeans())
])
# Définir la grille des paramètres pour GridSearchCV
# Change parameter names to directly reference the 'clusterer' step
param_grid = {
'clusterer__n_clusters': [5],
'clusterer__init': ['k-means++', 'random'],
'clusterer__n_init': [10, 20, 30],
'clusterer__max_iter': [300, 400, 500]
}
# Définir une fonction de scoring pour silhouette_score
def silhouette_scorer(estimator, X):
cluster_labels = estimator.fit_predict(X)
return silhouette_score(X, cluster_labels)
# Utiliser GridSearchCV pour trouver les meilleurs paramètres
grid_search = GridSearchCV(kmeans_pipe_2, param_grid, cv=5, scoring=silhouette_scorer)
grid_search.fit(df)
# Meilleurs paramètres
best_params = grid_search.best_params_
best_estimator = grid_search.best_estimator_
print("Meilleurs paramètres : ", best_params)
Meilleurs paramètres : {'clusterer__init': 'k-means++', 'clusterer__max_iter': 300, 'clusterer__n_clusters': 5, 'clusterer__n_init': 10}
best_pipeline = grid_search.best_estimator_
best_pipeline
Pipeline(steps=[('preprocessor',
Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca',
PCA(n_components=26, random_state=0)]])),
('clusterer', KMeans(n_clusters=5, n_init=10))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocessor',
Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca',
PCA(n_components=26, random_state=0)]])),
('clusterer', KMeans(n_clusters=5, n_init=10))])Pipeline(steps=[('encoding',
OneHotEncoder(cols=['favorite_category',
'favorite_payment_type',
'customer_state'])),
('features_scaling', MinMaxScaler()),
['pca', PCA(n_components=26, random_state=0)]])OneHotEncoder(cols=['favorite_category', 'favorite_payment_type',
'customer_state'])MinMaxScaler()
PCA(n_components=26, random_state=0)
KMeans(n_clusters=5, n_init=10)
# add predicted labels to the inital dataset
best_pipeline.fit(df)
df_labels = df.copy()
df_labels['predicted_label'] = best_pipeline['clusterer'].labels_
clusters_descr = clustering_interpretations(df_labels)
nb_orders 0.000000 total_products_ordered 0.000000 total_paid 62.844008 mean_photo_qty_for_ordered_prods 0.000000 mean_reviews_score 89.172582 recency 0.000000 mean_delivery_time 56.961438 Name: 0, dtype: float64
nb_orders 100.000000 total_products_ordered 100.000000 total_paid 8.277678 mean_photo_qty_for_ordered_prods 35.998537 mean_reviews_score 3.250982 recency 73.753592 mean_delivery_time 61.551125 Name: 1, dtype: float64
nb_orders 76.213040 total_products_ordered 49.481160 total_paid 0.000000 mean_photo_qty_for_ordered_prods 70.979928 mean_reviews_score 40.550728 recency 100.000000 mean_delivery_time 76.959111 Name: 2, dtype: float64
nb_orders 6.297305 total_products_ordered 5.930331 total_paid 100.000000 mean_photo_qty_for_ordered_prods 100.000000 mean_reviews_score 0.000000 recency 84.804203 mean_delivery_time 100.000000 Name: 3, dtype: float64
nb_orders 46.197509 total_products_ordered 8.475530 total_paid 44.743001 mean_photo_qty_for_ordered_prods 95.663399 mean_reviews_score 100.000000 recency 9.808986 mean_delivery_time 0.000000 Name: 4, dtype: float64
- Cluster 0 : Clients passifs avec dépenses modérées
- Caractéristiques principales :
- Nombre de commandes : 0.00
- Produits commandés : 0.00
- Total payé : 62.84
- Quantité moyenne de photos pour les produits commandés : 0.00
- Note moyenne des avis : 89.17
- Récence : 0.00 (inactif récemment)
- Temps moyen de livraison : 56.96
- Stratégies :
- Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir.
- Offres spéciales : Proposer des promotions pour stimuler les achats.
- Caractéristiques principales :
- Cluster 1 : Clients réguliers à faible dépense récente
- Caractéristiques principales :
- Nombre de commandes : 100.00
- Produits commandés : 100.00
- Total payé : 8.28
- Quantité moyenne de photos pour les produits commandés : 35.99
- Note moyenne des avis : 3.25
- Récence : 73.75
- Temps moyen de livraison : 61.55
- Stratégies :
- Fidélisation : Offrir des récompenses pour leurs achats fréquents.
- Optimisation des avis : Encourager à laisser des avis pour améliorer leur score moyen.
- Caractéristiques principales :
- Cluster 2 : Clients fidèles à haute commande mais faible dépense récente
- Caractéristiques principales :
- Nombre de commandes : 76.21
- Produits commandés : 49.48
- Total payé : 0.00
- Quantité moyenne de photos pour les produits commandés : 70.98
- Note moyenne des avis : 40.55
- Récence : 100.00
- Temps moyen de livraison : 76.96
- Stratégies :
- Réengagement : Offrir des promotions pour réactiver les achats.
- Amélioration de la livraison : Réduire les délais de livraison pour améliorer la satisfaction.
- Caractéristiques principales :
- Cluster 3 : Clients à haute dépense unique
- Caractéristiques principales :
- Nombre de commandes : 6.30
- Produits commandés : 5.93
- Total payé : 100.00
- Quantité moyenne de photos pour les produits commandés : 100.00
- Note moyenne des avis : 0.00
- Récence : 84.80
- Temps moyen de livraison : 100.00
- Stratégies :
- Fidélisation : Proposer des offres pour les inciter à effectuer d'autres achats importants.
- Amélioration de la livraison : Réduire les délais de livraison pour augmenter la satisfaction.
- Caractéristiques principales :
- Cluster 4 : Clients satisfaits à faible activité
- Caractéristiques principales :
- Nombre de commandes : 46.20
- Produits commandés : 8.48
- Total payé : 44.74
- Quantité moyenne de photos pour les produits commandés : 95.66
- Note moyenne des avis : 100.00
- Récence : 9.81
- Temps moyen de livraison : 0.00
- Stratégies :
- Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir.
- Incitations : Offrir des promotions pour stimuler les achats.
- Caractéristiques principales :
tested_models = test_init_stability(best_pipeline, df, 20)
Test 1 - ARI : 1.0 Test 2 - ARI : 1.0 Test 3 - ARI : 1.0 Test 4 - ARI : 1.0 Test 5 - ARI : 1.0 Test 6 - ARI : 1.0 Test 7 - ARI : 1.0 Test 8 - ARI : 1.0 Test 9 - ARI : 1.0 Test 10 - ARI : 1.0 Test 11 - ARI : 1.0 Test 12 - ARI : 1.0 Test 13 - ARI : 1.0 Test 14 - ARI : 0.6180049538252859 Test 15 - ARI : 1.0 Test 16 - ARI : 0.8541371803007943 Test 17 - ARI : 1.0 Test 18 - ARI : 0.6180049538252859 Test 19 - ARI : 1.0 Test 20 - ARI : 0.5350762786704614
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(0, len(tested_models)), [tested_models[x]['ari_score'] for x in tested_models], marker='o')
ax.set_title('ARI score by test')
ax.set_xlabel('Tests')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(0, len(tested_models)+1, 1))
ax.set_yticks(np.arange(0, 1.1, 0.1))
plt.show()
print('Average ARI :', sum([tested_models[x]['ari_score'] for x in tested_models]) / len(tested_models))
Average ARI : 0.9312611683310914
7. Stabilité du modèle dans le temps - Fréquence de mise à jour
min_date = pd.to_datetime(db_df['order_purchase_timestamp'].min()).to_pydatetime()
max_date = pd.to_datetime(db_df['order_purchase_timestamp'].max()).to_pydatetime()
print('The dataset contain data from {} to {}'.format(min_date.strftime('%Y-%m-%d'), max_date.strftime('%Y-%m-%d')))
The dataset contain data from 2016-09-04 to 2018-10-17
Cette fonction test_temporal_stability évalue la stabilité temporelle d'un modèle de clustering intégré dans un pipeline. Elle commence par créer une copie des données fournies et fixe un état aléatoire pour assurer la reproductibilité des résultats si un état aléatoire est spécifié. La période d'analyse commence à partir d'une date donnée ou de la première date disponible dans les données. La fonction utilise les données d'une première année comme référence pour former un modèle initial.
Les caractéristiques des données de référence sont calculées, et le modèle est ajusté avec ces données. Ensuite, mois par mois, des données supplémentaires sont ajoutées, et le modèle est réajusté à chaque étape. Après chaque ajustement, les étiquettes des clients du modèle de référence sont prédites avec les nouvelles données, et le score ARI (Adjusted Rand Index) est calculé pour mesurer la stabilité des clusters par rapport au modèle de référence initial.
Les résultats, comprenant les modèles ajustés et les scores ARI, sont stockés dans un dictionnaire. Le score ARI indique dans quelle mesure les clusters restent cohérents avec le temps, ce qui permet de déterminer la fréquence de mise à jour nécessaire du modèle pour maintenir une performance optimale. Cette méthode permet ainsi de surveiller et d'assurer la robustesse du modèle de clustering face à l'évolution des données.
def test_temporal_stability(pipeline, db_df, begin_at=None, random_state=None, rfm=False):
final_dict = {} # To store models and scores
data = db_df.copy()
# fix a random_state to help
if not pipeline['clusterer'].random_state and random_state:
pipeline['clusterer'].random_state = random_state
if begin_at:
min_date = pd.to_datetime(begin_at)
min_date = pd.to_datetime(data['order_purchase_timestamp'].min()).to_pydatetime()
max_date = pd.to_datetime(data['order_purchase_timestamp'].max()).to_pydatetime()
limit = min_date + relativedelta(years=+1)
# get the first year of data
ref_data = data[(data['order_purchase_timestamp'] >= min_date) & (data['order_purchase_timestamp'] < limit)]
# compute all features
ref_data = compute_features(ref_data.copy())
# store the unique ids of the ref customers
ref_cust_ids = ref_data.index
if rfm:
ref_data = ref_data[['recency', 'nb_orders', 'total_paid']]
# ref model
pipeline.fit(ref_data)
final_dict['ref'] = pipeline['clusterer']
ref_data_pca = pipeline['preprocessor'].transform(ref_data)
ref_model_labels = pipeline['clusterer'].labels_
print('Ref data: {} rows'.format(ref_data.shape[0]))
# Adds data of additional months until the end and train a model each time
i = 1 # test number
while limit < max_date:
# increase limit by 1 month (placed here to get all data even if the last month is not complete)
limit += relativedelta(months=+1)
# get data from the min date to the limit date
cur_data = data[(data['order_purchase_timestamp'] >= min_date) & (data['order_purchase_timestamp'] < limit)]
cur_data = compute_features(cur_data.copy())
if rfm:
cur_data = cur_data[['recency', 'nb_orders', 'total_paid']]
cur_cust_nb = cur_data.shape[0]
# fit model on all current data
pipeline.fit(cur_data)
# get rid of customers not in the ref model
cur_data = cur_data[cur_data.index.isin(ref_cust_ids)]
# predict labels of clients in the ref model (with their data updated)
cur_model_labels = pipeline.predict(cur_data)
# store current model
final_dict[i] = {'model': pipeline['clusterer']}
# predict labels for customers in ref data
cur_model_labels = pipeline['clusterer'].predict(ref_data_pca)
# compute ARI score
final_dict[i]['ari_score'] = adjusted_rand_score(ref_model_labels, cur_model_labels)
print('Ref data + {} months ({} rows): ARI = {} '.format(i, cur_cust_nb, final_dict[i]['ari_score']))
i += 1
return final_dict
temp_stab_dict = test_temporal_stability(kmeans_pipe, db_df, random_state=1)
Ref data: 23138 rows Ref data + 1 months (27351 rows): ARI = 0.48122767112792747 Ref data + 2 months (31696 rows): ARI = 0.3215179521467082 Ref data + 3 months (39503 rows): ARI = 0.6575759833281585 Ref data + 4 months (44731 rows): ARI = 0.5874430469601531 Ref data + 5 months (51820 rows): ARI = 0.32671750933207966 Ref data + 6 months (58414 rows): ARI = 0.47070841878674946 Ref data + 7 months (65411 rows): ARI = 0.2952388056524625 Ref data + 8 months (72234 rows): ARI = 0.5612280304306734 Ref data + 9 months (78486 rows): ARI = 0.41197864613319163 Ref data + 10 months (84520 rows): ARI = 0.505904068292167 Ref data + 11 months (90911 rows): ARI = 0.5091041075873413 Ref data + 12 months (96091 rows): ARI = 0.9903698161395945 Ref data + 13 months (96095 rows): ARI = 0.2741040702988693 Ref data + 14 months (96096 rows): ARI = 0.6461730696608041
temp_stab_dict = test_temporal_stability(best_pipeline, db_df, random_state=1)
Ref data: 23138 rows Ref data + 1 months (27351 rows): ARI = 1.0 Ref data + 2 months (31696 rows): ARI = 0.5238007193013058 Ref data + 3 months (39503 rows): ARI = 0.9923447379790604 Ref data + 4 months (44731 rows): ARI = 0.99298057164426 Ref data + 5 months (51820 rows): ARI = 0.5257365018892318 Ref data + 6 months (58414 rows): ARI = 0.983773652512818 Ref data + 7 months (65411 rows): ARI = 0.9829029913862524 Ref data + 8 months (72234 rows): ARI = 0.9942533916458502 Ref data + 9 months (78486 rows): ARI = 0.9942533916458502 Ref data + 10 months (84520 rows): ARI = 0.9943807580560333 Ref data + 11 months (90911 rows): ARI = 0.5553112768756359 Ref data + 12 months (96091 rows): ARI = 0.9954002409122074 Ref data + 13 months (96095 rows): ARI = 0.5130918018502667 Ref data + 14 months (96096 rows): ARI = 0.9952727519678491
Les scores ARI montrent une forte stabilité du modèle pour plusieurs mois, mais des baisses significatives apparaissent après 2, 5, 11, et 13 mois. Pour maintenir la fiabilité, il est recommandé de mettre à jour le modèle tous les 1 à mois.
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(1, len(temp_stab_dict)), [temp_stab_dict[x]['ari_score'] for x in temp_stab_dict if x != 'ref'], marker='o')
ax.set_title('ARI score evolution in time')
ax.set_xlabel('Additional months from baseline')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(1, len(temp_stab_dict), 1))
ax.set_yticks(np.arange(0, 1.2, 0.1))
plt.show()
B. K-Means avec méthode RFM
kmeans_rfm_pipe = Pipeline([
('preprocessor', StandardScaler()),
('clusterer', KMeans())
])
df_rfm_kmeans = df[['recency', 'total_paid', 'nb_orders']].copy()
1. Nombre optimal de clusters
k_range = (2, 11)
display_elbow_visu(kmeans_rfm_pipe, df_rfm_kmeans, k_range, 'distortion')
display_elbow_visu(kmeans_rfm_pipe, df_rfm_kmeans, k_range, 'silhouette')
kmeans_rfm_pipe['clusterer'].n_clusters = 4
2. Visualisation
kmeans_rfm_pipe.fit(df_rfm_kmeans)
Pipeline(steps=[('preprocessor', StandardScaler()),
('clusterer', KMeans(n_clusters=4))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Pipeline(steps=[('preprocessor', StandardScaler()),
('clusterer', KMeans(n_clusters=4))])StandardScaler()
KMeans(n_clusters=4)
df_rfm_kmeans_labels = df_rfm_kmeans.copy()
df_rfm_kmeans_labels['predicted_label'] = kmeans_rfm_pipe['clusterer'].labels_
rfm_iso_scatter(df_rfm_kmeans_labels, 'recency', 'nb_orders', 'total_paid', color='predicted_label', reduce=True)
%%time
tsne_visualization(kmeans_rfm_pipe['preprocessor'].transform(df_rfm_kmeans), kmeans_rfm_pipe['clusterer'].labels_, tsne_params)
CPU times: total: 31min 16s Wall time: 16min 55s
3. Interprêtation des clusters
clusters_descr_rfm = clustering_interpretations(df_rfm_kmeans_labels, (0, 4))
recency 4.000000 total_paid 0.002496 nb_orders 0.000000 Name: 0, dtype: float64
recency 1.755021 total_paid 4.000000 nb_orders 0.086754 Name: 1, dtype: float64
recency 0.0 total_paid 0.0 nb_orders 0.0 Name: 2, dtype: float64
recency 1.390199 total_paid 0.437025 nb_orders 4.000000 Name: 3, dtype: float64
- Cluster 0 : Clients inactifs
- Caractéristiques principales :
- Récence : 4.00 (ancien)
- Total payé : 0.002496
- Nombre de commandes (nb_orders) : 0.00
- Stratégies :
- Réactivation : Offrir des incitations pour revenir, telles que des coupons ou des promotions.
- Engagement : Envoyer des campagnes de marketing par email pour susciter l'intérêt.
- Caractéristiques principales :
- Cluster 1 : Clients à forte dépense
- Caractéristiques principales :
- Récence : 1.76
- Total payé : 4.00 (très élevé)
- Nombre de commandes (nb_orders) : 0.09
- Stratégies :
- Programmes de fidélité : Proposer des récompenses exclusives pour les inciter à continuer leurs achats.
- Offres personnalisées : Envoyer des offres basées sur leurs historiques d'achats pour maximiser l'engagement.
- Caractéristiques principales :
- Cluster 2 : Clients très inactifs
- Caractéristiques principales :
- Récence : 0.00
- Total payé : 0.00
- Nombre de commandes (nb_orders) : 0.00
- Stratégies :
- Réengagement : Utiliser des campagnes de réactivation pour les inciter à revenir, comme des emails ou des notifications push.
- Incitations : Offrir des réductions ou des avantages pour leur premier achat après une période d'inactivité.
- Caractéristiques principales :
- Cluster 3 : Clients fréquents
- Caractéristiques principales :
- Récence : 1.39
- Total payé : 0.44
- Nombre de commandes (nb_orders) : 4.00 (très élevé)
- Stratégies :
- Fidélisation : Offrir des récompenses pour les achats fréquents, comme des programmes de points ou des cartes de fidélité.
- Personnalisation : Proposer des recommandations de produits basées sur leurs habitudes d'achat pour maximiser leur satisfaction.
- Caractéristiques principales :
clusters_label_rfm = {
0: 'Lost customers, low spenders',
1: 'New customers, low spenders',
2: 'Best customers, loyalists'
}
df_rfm_kmeans_labels['cluster_interpretation'] = df_rfm_kmeans_labels['predicted_label'].apply(lambda x: clusters_label_rfm[x])
df_recap_kmeans_rfm = df_rfm_kmeans_labels.reset_index().groupby('predicted_label').agg(
Count=('customer_unique_id', 'count'),
Recency=('recency', 'mean'),
Frequency=('nb_orders', 'mean'),
Monetary=('total_paid', 'mean'),
ClusterInterpretation=('cluster_interpretation', 'unique')
).round(1)
df_recap_kmeans_rfm
4. Stabilité du modèle
tested_models = test_init_stability(kmeans_rfm_pipe, df_rfm_kmeans, 10)
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(0, len(tested_models)), [tested_models[x]['ari_score'] for x in tested_models], marker='o')
ax.set_title('ARI score evolution')
ax.set_xlabel('Tests')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(0, len(tested_models)+1, 1))
ax.set_yticks(np.arange(0, 1.2, 0.1))
plt.show()
Test 1 - ARI : 0.9653289329612601 Test 2 - ARI : 0.9703162644519343 Test 3 - ARI : 0.9699059488929386 Test 4 - ARI : 0.9564521879590366 Test 5 - ARI : 0.9703162644519343 Test 6 - ARI : 0.9699059488929386 Test 7 - ARI : 0.9699059488929386 Test 8 - ARI : 0.9703162644519343 Test 9 - ARI : 0.9703162644519343 Test 10 - ARI : 0.9644294770078878
Ces résultats indiquent une stabilité générale du modèle avec des scores ARI élevés et constants, principalement autour de 0.97. Les légères variations observées, comme une baisse à 0.9565 lors du Test 4 et à 0.9644 lors du Test 10, montrent que les clusters restent très cohérents malgré l'ajout de nouvelles données. La constance des scores ARI proches de 0.97 suggère que le modèle est fiable et maintient une bonne stabilité au fil du temps.
5. Fréquence de mise à jour du modèle
temp_stab_dict_rfm = test_temporal_stability(kmeans_rfm_pipe, db_df, random_state=15, rfm=True)
Ref data: 23138 rows Ref data + 1 months (27351 rows): ARI = 0.9324223274156803 Ref data + 2 months (31696 rows): ARI = 0.9343698091101679 Ref data + 3 months (39503 rows): ARI = 0.9780179523856091 Ref data + 4 months (44731 rows): ARI = 0.9938465222062125 Ref data + 5 months (51820 rows): ARI = 0.9891774676981612 Ref data + 6 months (58414 rows): ARI = 0.9644491109587955 Ref data + 7 months (65411 rows): ARI = 0.9271382850915354 Ref data + 8 months (72234 rows): ARI = 0.907835046786118 Ref data + 9 months (78486 rows): ARI = 0.9075800923671268 Ref data + 10 months (84520 rows): ARI = 0.8906574968524584 Ref data + 11 months (90911 rows): ARI = 0.890771437294046 Ref data + 12 months (96091 rows): ARI = 0.9488063885449789 Ref data + 13 months (96095 rows): ARI = 0.4120834175020492 Ref data + 14 months (96096 rows): ARI = 0.9286967699968822
Les résultats montrent les scores ARI (Adjusted Rand Index) pour chaque mois additionnel de données par rapport au modèle de référence.
- Ref data (23138 rows) : Ce sont les données de référence utilisées pour le modèle initial.
- Ref data + 1 mois à +12 mois : Les scores ARI restent relativement élevés, variant entre 0.89 et 0.99, indiquant une forte stabilité des clusters malgré l'ajout de nouvelles données. Ceci montre que le modèle reste fiable pour ces périodes.
- Ref data + 13 mois (96095 rows) : Il y a une chute significative du score ARI à 0.412, indiquant une perte majeure de stabilité du modèle. Cela signifie que les clusters ont changé de manière significative après 13 mois.
- Ref data + 14 mois (96096 rows) : Le score ARI remonte à 0.928, ce qui est surprenant après la chute drastique du mois précédent. Cela pourrait indiquer une anomalie dans les données ou un réajustement inattendu des clusters.
D'après ces observations, il est recommandé de renouveler le modèle tous les 12 mois pour maintenir une bonne stabilité des clusters.
f, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.arange(1, len(temp_stab_dict_rfm)), [temp_stab_dict_rfm[x]['ari_score'] for x in temp_stab_dict_rfm if x != 'ref'], marker='o')
ax.set_title('ARI score evolution in time')
ax.set_xlabel('Additional months from baseline')
ax.set_ylabel('ARI score')
ax.set_xticks(np.arange(1, len(temp_stab_dict_rfm), 1))
ax.set_yticks(np.arange(0, 1.2, 0.1))
plt.show()
C. Comparaison de deux K-Means
En comparant les deux modèles, on se rend compte que le modèle avec RFM est meilleur car il est plus stable. Le modèle RFM utilise les variables de recency, de nombre de commandes (nb_orders) et de montant total payé (total_paid), ce qui permet de mieux capturer le comportement d'achat des clients en termes de fréquence, de récence et de montant dépensé. Ces critères sont essentiels pour segmenter les clients en fonction de leur valeur et de leur engagement avec l'entreprise.
En revanche, le modèle sans RFM utilise une gamme plus large de variables, y compris le nombre total de produits commandés (total_products_ordered), la quantité moyenne de photos par produit commandé (mean_photo_qty_for_ordered_prods), la note moyenne des avis (mean_reviews_score) et le temps moyen de livraison (mean_delivery_time). Bien que ces variables fournissent une vue plus détaillée et diversifiée du comportement des clients, elles peuvent introduire plus de variabilité et complexité, rendant le modèle moins stable.
Cependant, il serait difficile de les comparer directement étant donné que les variables ne sont pas les mêmes et que chaque méthode a un objectif défini. Le modèle RFM est spécifiquement conçu pour les analyses de valeur client, tandis que le modèle sans RFM peut offrir des insights plus détaillés mais potentiellement moins cohérents en termes de segmentation stable des clients.
II. Gaussian Mixture Models
Le Gaussian Mixture Model (GMM) est une technique avancée de clustering qui repose sur le principe de mélange gaussien. Ce modèle suppose que les données proviennent d'une combinaison de plusieurs distributions normales (ou gaussiennes), chacune correspondant à un cluster distinct. Chaque distribution est définie par ses propres paramètres : moyenne, covariance et proportion. Le but du GMM est d'estimer ces paramètres de manière à mieux représenter la distribution globale des données.
Contrairement à des méthodes comme K-means, qui attribue chaque point de données à un cluster unique en se basant sur la distance euclidienne, le GMM offre une approche plus flexible et probabiliste. Il permet de modéliser des clusters de formes ellipsoïdales et non simplement sphériques, ce qui le rend particulièrement utile pour des ensembles de données où les clusters ont des formes et tailles variées.
Le GMM utilise un algorithme d'espérance-maximisation (EM) pour ajuster les paramètres des distributions gaussiennes. L'algorithme fonctionne en deux étapes :
- Étape d'espérance (E-step) : Calcul des probabilités d'appartenance de chaque point de données à chaque composant (distribution) du mélange.
- Étape de maximisation (M-step) : Mise à jour des paramètres des distributions en maximisant la probabilité que les données observées proviennent de ce modèle.
Ce processus itératif continue jusqu'à ce que la convergence soit atteinte, c'est-à-dire que les changements dans les paramètres deviennent négligeables. En fin de compte, chaque point de données est attribué à un cluster avec une certaine probabilité, permettant une classification plus nuancée et potentiellement plus précise des données complexes.
Le GMM est couramment utilisé dans divers domaines, notamment la reconnaissance de formes, la bioinformatique, l'analyse de texte, et partout où une modélisation flexible des données est nécessaire.
A. Clustering
gmm_params = {
'n_components': kmeans_pipe['clusterer'].n_clusters,
'max_iter': 1000,
'tol': 1e-4,
'init_params': 'kmeans',
'random_state': 1
}
gmm_pipe = Pipeline([
('preprocessor', preprocessor),
('clusterer', GaussianMixture(**gmm_params))
])
gmm_pipe.fit(df)
preprocessed_data_gmm = gmm_pipe['preprocessor'].transform(df)
predicted_labels_gmm = gmm_pipe['clusterer'].predict(preprocessed_data_gmm)
print('Nb iterations to converge: ', gmm_pipe['clusterer'].n_iter_)
print('Silouhette score:', silhouette_score(preprocessed_data_gmm, predicted_labels_gmm))
kmeans_pipe.fit(df)
print(f"Comparison with kmeans predictions : ARI={adjusted_rand_score(kmeans_pipe['clusterer'].labels_, predicted_labels_gmm)}")
Comparison with kmeans predictions : ARI=0.7488953672558403
B. Visualisation
tsne_visualization(preprocessed_data_gmm, predicted_labels_gmm, tsne_params)
df_labels_gmm = df.copy()
df_labels_gmm['predicted_label'] = predicted_labels_gmm
clustering_interpretations(df_labels_gmm)
nb_orders 5.365896 total_products_ordered 8.127012 total_paid 93.597394 mean_photo_qty_for_ordered_prods 0.000000 mean_reviews_score 85.781933 recency 0.000000 mean_delivery_time 16.160215 Name: 0, dtype: float64
nb_orders 100.000000 total_products_ordered 79.577252 total_paid 0.000000 mean_photo_qty_for_ordered_prods 64.516846 mean_reviews_score 47.696830 recency 72.687194 mean_delivery_time 100.000000 Name: 1, dtype: float64
nb_orders 48.885116 total_products_ordered 7.007017 total_paid 79.724968 mean_photo_qty_for_ordered_prods 27.712337 mean_reviews_score 0.000000 recency 15.291593 mean_delivery_time 9.363278 Name: 2, dtype: float64
nb_orders 88.658089 total_products_ordered 100.000000 total_paid 86.304833 mean_photo_qty_for_ordered_prods 85.810186 mean_reviews_score 34.576523 recency 33.951955 mean_delivery_time 50.365285 Name: 3, dtype: float64
nb_orders 0.0 total_products_ordered 0.0 total_paid 100.0 mean_photo_qty_for_ordered_prods 100.0 mean_reviews_score 100.0 recency 100.0 mean_delivery_time 0.0 Name: 4, dtype: float64
| nb_orders | total_products_ordered | total_paid | mean_photo_qty_for_ordered_prods | mean_reviews_score | recency | mean_delivery_time | |
|---|---|---|---|---|---|---|---|
| 0 | 5.365896 | 8.127012 | 93.597394 | 0.000000 | 85.781933 | 0.000000 | 16.160215 |
| 1 | 100.000000 | 79.577252 | 0.000000 | 64.516846 | 47.696830 | 72.687194 | 100.000000 |
| 2 | 48.885116 | 7.007017 | 79.724968 | 27.712337 | 0.000000 | 15.291593 | 9.363278 |
| 3 | 88.658089 | 100.000000 | 86.304833 | 85.810186 | 34.576523 | 33.951955 | 50.365285 |
| 4 | 0.000000 | 0.000000 | 100.000000 | 100.000000 | 100.000000 | 100.000000 | 0.000000 |
- Cluster 0 : Clients récents et satisfaits
- Caractéristiques principales :
- Nombre de commandes (nb_orders) : 5.37
- Total des produits commandés : 8.13
- Total payé : 93.60
- Score moyen des avis : 85.78
- Récence (recency) : 0.00 (très récent)
- Délai moyen de livraison : 16.16 jours
- Stratégies :
- Fidélisation : Offrir des programmes de fidélité pour encourager les achats répétés.
- Promotions ciblées : Envoyer des promotions sur les nouveaux produits pour maintenir leur intérêt.
- Feedback : Solliciter des avis pour continuer à améliorer l'expérience client.
- Caractéristiques principales :
- Cluster 1 : Acheteurs fréquents à faible dépense
- Caractéristiques principales :
- Nombre de commandes (nb_orders) : 100.00 (très élevé)
- Total des produits commandés : 79.58
- Total payé : 0.00 (très faible)
- Score moyen des avis : 47.70
- Récence : 72.69
- Délai moyen de livraison : 100.00 jours
- Stratégies :
- Augmentation du panier moyen : Proposer des ventes croisées et des offres groupées pour augmenter la dépense moyenne.
- Amélioration de la satisfaction : Réduire le délai de livraison et améliorer la qualité des produits.
- Avis et retours : Analyser les avis pour identifier les points d'amélioration.
- Caractéristiques principales :
- Cluster 2 : Clients occasionnels avec avis neutres
- Caractéristiques principales :
- Nombre de commandes (nb_orders) : 48.89
- Total des produits commandés : 7.01
- Total payé : 79.72
- Score moyen des avis : 0.00 (aucun avis donné)
- Récence : 15.29
- Délai moyen de livraison : 9.36 jours
- Stratégies :
- Engagement : Encourager les avis clients pour obtenir des retours.
- Re-marketing : Utiliser des campagnes de re-marketing pour les inciter à revenir.
- Expérience client : Améliorer l'expérience d'achat pour transformer ces clients en acheteurs réguliers.
- Caractéristiques principales :
- Cluster 3 : Clients actifs et diversifiés
- Caractéristiques principales :
- Nombre de commandes (nb_orders) : 88.66
- Total des produits commandés : 100.00
- Total payé : 86.30
- Score moyen des avis : 34.58
- Récence : 33.95
- Délai moyen de livraison : 50.37 jours
- Stratégies :
- Programme de fidélité : Créer des programmes de fidélité pour renforcer leur engagement.
- Amélioration de la livraison : Réduire les délais de livraison pour améliorer la satisfaction.
- Personnalisation des offres : Proposer des offres personnalisées basées sur leurs habitudes d'achat.
- Caractéristiques principales :
- Cluster 4 : Clients premium et exigeants
- Caractéristiques principales :
- Nombre de commandes (nb_orders) : 0.00
- Total des produits commandés : 0.00
- Total payé : 100.00
- Score moyen des avis : 100.00
- Récence : 100.00
- Délai moyen de livraison : 0.00
- Stratégies :
- Maintien de la qualité : Assurer un niveau de service élevé pour répondre à leurs attentes.
- Offres exclusives : Offrir des produits exclusifs et des expériences premium.
- Feedback proactif : Obtenir des retours détaillés pour continuer à s'améliorer.
- Caractéristiques principales :